<?php

class Message
{
    public $salt;
    public $ts;
    public $privileges;

    public function __construct(){
        $this->salt = random_int(1,99999999);

        $date = new DateTime("now",new DateTimeZone('UTC'));
        $this->ts = $date->getTimestamp() + 24 * 3600;

        $this->privileges = array();
    }

    public function packContent(){
        $buffer = unpack("C*",pack("V",$this->salt));
        $buffer = array_merge($buffer,unpack("C*",pack("V",$this->ts)));
        $buffer = array_merge($buffer,unpack("C*",pack("v",sizeof($this->privileges))));
        foreach($this->privileges as $key => $value){
            $buffer = array_merge($buffer,unpack("C*",pack("v",$key)));
            $buffer = array_merge($buffer,unpack("C*",pack("V",$value)));
        }
        return $buffer;
    }

    public function unpackContent($msg){
        $pos = 0;
        $salt = unpack("V",substr($msg,$pos,4))[1];
        $pos += 4;
        $ts = unpack("V",substr($msg,$pos,4))[1];
        $pos += 4;
        $size = unpack("v",substr($msg,$pos,2))[1];
        $pos += 2;

        $privileges = array();
        for($i = 0;$i < $size;$i++){
            $key = unpack("v",substr($msg,$pos,2));
            $pos += 2;
            $value = unpack("V",substr($msg,$pos,4));
            $pos += 4;
            $privileges[$key[1]] = $value[1];
        }
        $this->salt = $salt;
        $this->ts = $ts;
        $this->privileges = $privileges;
    }
}

class AccessToken
{
    const Privileges = array(
        "kJoinChannel" => 1,
        "kPublishAudioStream" => 2,
        "kPublishVideoStream" => 3,
        "kPublishDataStream" => 4,
        "kPublishAudioCdn" => 5,
        "kPublishVideoCdn" => 6,
        "kRequestPublishAudioStream" => 7,
        "kRequestPublishVideoStream" => 8,
        "kRequestPublishDataStream" => 9,
        "kInvitePublishAudioStream" => 10,
        "kInvitePublishVideoStream" => 11,
        "kInvitePublishDataStream" => 12,
        "kAdministrateChannel" => 101,
        "kRtmLogin" => 1000,
    );

    public $appID,$appCertificate,$channelName,$uid;
    public $message;

    function __construct(){
        $this->message = new Message();
    }

    function setUid($uid){
        if($uid === 0){
            $this->uid = "";
        }else{
            $this->uid = $uid . '';
        }
    }

    function is_nonempty_string($name,$str){
        if(is_string($str) && $str !== ""){
            return true;
        }
        echo $name . " check failed, should be a non-empty string";
        return false;
    }

    static function init($appID,$appCertificate,$channelName,$uid){
        $accessToken = new AccessToken();

        if(!$accessToken->is_nonempty_string("appID",$appID) ||
            !$accessToken->is_nonempty_string("appCertificate",$appCertificate) ||
            !$accessToken->is_nonempty_string("channelName",$channelName)){
            return null;
        }

        $accessToken->appID = $appID;
        $accessToken->appCertificate = $appCertificate;
        $accessToken->channelName = $channelName;

        $accessToken->setUid($uid);
        $accessToken->message = new Message();
        return $accessToken;
    }

    static function initWithToken($token,$appCertificate,$channel,$uid){
        $accessToken = new AccessToken();
        if(!$accessToken->extract($token,$appCertificate,$channel,$uid)){
            return null;
        }
        return $accessToken;
    }

    function addPrivilege($key,$expireTimestamp){
        $this->message->privileges[$key] = $expireTimestamp;
        return $this;
    }

    function extract($token,$appCertificate,$channelName,$uid){
        $ver_len = 3;
        $appid_len = 32;
        $version = substr($token,0,$ver_len);
        if($version !== "006"){
            echo 'invalid version ' . $version;
            return false;
        }

        if(!$this->is_nonempty_string("token",$token) ||
            !$this->is_nonempty_string("appCertificate",$appCertificate) ||
            !$this->is_nonempty_string("channelName",$channelName)){
            return false;
        }

        $appid = substr($token,$ver_len,$appid_len);
        $content = (base64_decode(substr($token,$ver_len + $appid_len,strlen($token) - ($ver_len + $appid_len))));

        $pos = 0;
        $len = unpack("v",$content . substr($pos,2))[1];
        $pos += 2;
        $sig = substr($content,$pos,$len);
        $pos += $len;
        $crc_channel = unpack("V",substr($content,$pos,4))[1];
        $pos += 4;
        $crc_uid = unpack("V",substr($content,$pos,4))[1];
        $pos += 4;
        $msgLen = unpack("v",substr($content,$pos,2))[1];
        $pos += 2;
        $msg = substr($content,$pos,$msgLen);

        $this->appID = $appid;
        $message = new Message();
        $message->unpackContent($msg);
        $this->message = $message;

        //non reversable values
        $this->appCertificate = $appCertificate;
        $this->channelName = $channelName;
        $this->setUid($uid);
        return true;
    }

    function build(){
        $msg = $this->message->packContent();
        $val = array_merge(unpack("C*",$this->appID),unpack("C*",$this->channelName),unpack("C*",$this->uid),$msg);

        $sig = hash_hmac('sha256',implode(array_map("chr",$val)),$this->appCertificate,true);

        $crc_channel_name = crc32($this->channelName) & 0xffffffff;
        $crc_uid = crc32($this->uid) & 0xffffffff;

        $content = array_merge(unpack("C*",packString($sig)),unpack("C*",pack("V",$crc_channel_name)),unpack("C*",pack("V",$crc_uid)),unpack("C*",pack("v",count($msg))),$msg);
        $version = "006";
        $ret = $version . $this->appID . base64_encode(implode(array_map("chr",$content)));
        return $ret;
    }
}

function packString($value){
    return pack("v",strlen($value)) . $value;
}
